package net.seratch.taskun.scheduler.util;

import java.util.ArrayList;
import java.util.Calendar;
import java.util.List;

import net.seratch.taskun.scheduler.crond.Crontab;
import net.seratch.taskun.util.CalendarUtil;
import net.seratch.taskun.util.StringUtil;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

/**
 * Crontab parsing utility
 * 
 * @author Kazuhiro Sera
 */
public class CrontabUtil {

	protected static Log log = LogFactory.getLog(CrontabUtil.class);

	/**
	 * Read a line of the crontab text file
	 * 
	 * @param line
	 * @return
	 */
	public static Crontab read(String line) {
		Crontab crontab = new Crontab();
		line = line.trim();
		if (line.equals("") || line.startsWith("#")) {
			return null;
		}
		crontab.line = line;
		try {
			String[] elements = line.split("\\s+");
			if (elements[0].matches("interval:\\d+?sec")) {
				return getIntervalInvocationCrontab(crontab, elements);
			} else {
				return getNormalCrontab(crontab, elements);
			}
		} catch (ArrayIndexOutOfBoundsException e) {
			log.error("Invalid crontab expression...");
			throw new IllegalArgumentException("Invalid crontab expression...");
		}
	}

	/**
	 * Get the configuration of interval invocation
	 * 
	 * @param crontab
	 *            crontab object
	 * @param elements
	 *            elements in the line
	 * @return crontab object
	 */
	protected static Crontab getIntervalInvocationCrontab(Crontab crontab,
			String[] elements) {
		crontab.isIntervalInvocation = true;
		crontab.intervalSeconds = getLongValue(elements[0].split(":")[1]);
		crontab.initialIntervalSeconds = getLongValue(elements[1].split(":")[1]);
		crontab.commandClassName = getCommandClassName(elements[2]);
		crontab.multiplicity = getMultiplicity(elements[2]);
		if (elements.length == 4 && !StringUtil.isEmpty(elements[3])) {
			String[] servers = elements[3].split(",");
			crontab.namedServers = new ArrayList<String>();
			for (String server : servers) {
				if (!StringUtil.isEmpty(server)) {
					crontab.namedServers.add(server);
				}
			}
		}
		return crontab;
	}

	/**
	 * Get long number value from string value
	 * 
	 * @param number
	 *            long value in string
	 * @return long value
	 */
	protected static long getLongValue(String number) {
		return Long.valueOf(number.replaceAll("[^\\d]+", ""));
	}

	/**
	 * Read multiplicity configuration
	 * 
	 * @param element
	 *            element in the line
	 * @return multiplicity configuration
	 */
	protected static long getMultiplicity(String element) {
		String[] arr = element.split("\\*");
		if (arr.length == 2) {
			return Long.valueOf(arr[1]);
		} else {
			return 1L;
		}
	}

	/**
	 * Get Runnable command class name
	 * 
	 * @param element
	 *            element in the line
	 * @return result
	 */
	protected static String getCommandClassName(String element) {
		return element.split("\\*")[0];
	}

	/**
	 * Get the configuration of crontab invocation
	 * 
	 * @param crontab
	 *            crontab object
	 * @param elements
	 *            elements in the line
	 * @return crontab object
	 */
	protected static Crontab getNormalCrontab(Crontab crontab, String[] elements) {
		crontab.minute = elements[0];
		crontab.minuteFixedInvocations = getFixedInvocations(59, crontab.minute);
		crontab.hour = elements[1];
		crontab.hourFixedInvocations = getFixedInvocations(23, crontab.hour);
		crontab = replaceWildcardsOfMinuteOrHourValue(crontab);
		crontab.day = elements[2];
		if (isNotWildcard(crontab.day)) {
			crontab.dayFixedInvocations = getFixedInvocations(31, crontab.day);
		}
		crontab.month = elements[3];
		if (isNotWildcard(crontab.month)) {
			crontab.monthFixedInvocations = getFixedInvocations(12,
					crontab.month);
		}
		crontab.dayOfWeek = elements[4];
		if (isNotWildcard(crontab.dayOfWeek)) {
			crontab.dayOfWeekFixedInvocations = getFixedInvocations(7,
					crontab.dayOfWeek);
		}
		crontab.commandClassName = getCommandClassName(elements[5]);
		crontab.multiplicity = getMultiplicity(elements[5]);
		crontab.nextInvocationTime = getNextInvocationTime(
				CalendarUtil.getCurrentTime(), crontab);
		if (elements.length == 7 && !StringUtil.isEmpty(elements[6])) {
			String[] servers = elements[6].split(",");
			crontab.namedServers = new ArrayList<String>();
			for (String server : servers) {
				if (!StringUtil.isEmpty(server)) {
					crontab.namedServers.add(server);
				}
			}
		}
		return crontab;
	}

	/**
	 * Get the fixed invocations from crontab element
	 * 
	 * @param maxValue
	 *            max value in the element
	 * @param crontabValue
	 *            crontab element
	 * @return the fixed invocations
	 */
	public static List<Integer> getFixedInvocations(int maxValue,
			String crontabValue) {
		int begin = 0;
		int period = 1;
		int end = maxValue;
		List<Integer> invocations = new ArrayList<Integer>();
		String[] slashDelimited = crontabValue.split("/");
		if (crontabValue.equals("*")) {
			for (int i = 0; i <= maxValue; i++) {
				invocations.add(i);
			}
			return invocations;
		} else if (slashDelimited.length == 1
				&& crontabValue.matches("^[\\d,]+$")) {
			String[] commaDelimited = crontabValue.split(",");
			int len = commaDelimited.length;
			for (int i = 0; i < len; i++) {
				invocations.add(Integer.valueOf(commaDelimited[i]));
			}
			return invocations;
		} else if (slashDelimited.length == 2) {
			String leftSide = slashDelimited[0];
			if (leftSide.matches("^\\d+$")) {
				begin = Integer.valueOf(slashDelimited[0]);
			} else if (leftSide.matches("^\\d+?-\\d+$")) {
				String[] beginAndEnd = slashDelimited[0].split("-");
				begin = Integer.valueOf(beginAndEnd[0]);
				end = Integer.valueOf(beginAndEnd[1]);
			}
			period = Integer.valueOf(slashDelimited[1]);
		} else {
			throw new IllegalArgumentException(
					"Invalid crontab configuration value:" + crontabValue);
		}
		int invocation = begin;
		while (invocation <= end) {
			invocations.add(invocation);
			invocation += period;
		}
		return invocations;
	}

	/**
	 * Get next invocation time from crontab configuration
	 * 
	 * @param currentTime
	 *            current time
	 * @param crontab
	 *            crontab configuration
	 * @return next invocation time in milliseconds
	 */
	public static long getNextInvocationTime(Calendar currentTime,
			Crontab crontab) {
		int minute = CalendarUtil.getMinute(currentTime);
		if (isFixedValue(crontab.minute)) {
			minute = getFixedNextInvocationValue(minute, 59, crontab.minute);
		} else {
			minute = getFixedNextInvocationValue(minute,
					crontab.minuteFixedInvocations);
		}
		int hour = CalendarUtil.get24Hour(currentTime);
		if (isFixedValue(crontab.hour)) {
			hour = Integer.valueOf(crontab.hour);
		}
		crontab = replaceWildcardsOfMinuteOrHourValue(crontab);
		int day = CalendarUtil.getDay(currentTime);
		if (isFixedValue(crontab.day)) {
			day = Integer.valueOf(crontab.day);
		}
		int dayOfWeek = getCrontabDayOfWeekNumber(currentTime);
		if (dayOfWeek >= 7) {
			dayOfWeek = dayOfWeek - 7;
		}
		int month = CalendarUtil.getMonth(currentTime);
		if (isFixedValue(crontab.month)) {
			month = Integer.valueOf(crontab.month);
		}
		int year = CalendarUtil.getYear(currentTime);
		// next invocation millisecs
		Calendar nextInvocationTime = CalendarUtil.getCalendar(year, month,
				day, hour, minute, 0);

		boolean isMinuteModified = false;
		boolean isHourModified = false;
		boolean isDayModified = false;
		boolean isMonthModified = false;
		int loopCounter = 0;

		// day of week type
		if (isNotWildcard(crontab.dayOfWeek)) {
			// 1. next invocation time in millis is larger than current time
			// or
			// 2. day of week value to invoke next is defined one in crontab
			while (nextInvocationTime.getTimeInMillis() < currentTime
					.getTimeInMillis()
					|| (getCrontabDayOfWeekNumber(nextInvocationTime) != getFixedNextInvocationValue(
							dayOfWeek, 6, crontab.dayOfWeek) && !isInvocationValueContainedInCrontabValue(
							getCrontabDayOfWeekNumber(nextInvocationTime), 6,
							crontab.dayOfWeek))) {
				if (loopCounter >= 100) {
					throw new IllegalStateException(
							"Cannot get next invocation time of crontab scheduled task...");
				}
				if (isMinuteModified || !isNotWildcard(crontab.minute)) {
					if (isHourModified || isNotWildcard(crontab.hour)) {
						if (isHourModified || isFixedValue(crontab.hour)) {
							if (isDayModified) {
								if (isMonthModified
										|| isNotWildcard(crontab.month)) {
									if (isMonthModified
											|| isFixedValue(crontab.month)) {
										// cannot specify year at crontab
										nextInvocationTime
												.add(Calendar.YEAR, 1);
									} else {
										nextInvocationTime.set(
												Calendar.MONTH,
												getFixedNextInvocationValue(
														month, 12,
														crontab.month));
										isMonthModified = true;
									}
								} else {
									nextInvocationTime.add(Calendar.MONTH, 1);
									isMonthModified = true;
								}
							} else {
								boolean isNotContainedInCrontab = !isInvocationValueContainedInCrontabValue(
										dayOfWeek, 6, crontab.dayOfWeek);
								boolean isAfterFixedTimestamp = (CalendarUtil
										.get24Hour(currentTime) * 60 + CalendarUtil
										.getMinute(currentTime)) > (hour * 60 + minute);
								boolean isNextInvocationNotInThisDate = currentTime
										.getTimeInMillis() > nextInvocationTime
										.getTimeInMillis();
								if (isNotContainedInCrontab
										|| isAfterFixedTimestamp
										|| isNextInvocationNotInThisDate) {
									// add days until next day of week
									int addDays = 0;
									int nextDayOfWeek = getFixedNextInvocationValue(
											dayOfWeek, 6, crontab.dayOfWeek);
									if (nextDayOfWeek > dayOfWeek) {
										addDays = nextDayOfWeek - dayOfWeek;
									} else {
										addDays = nextDayOfWeek - dayOfWeek + 7;
									}
									nextInvocationTime.add(Calendar.DATE,
											addDays);
								}
								isDayModified = true;
							}
						} else {
							nextInvocationTime.set(
									Calendar.HOUR_OF_DAY,
									getFixedNextInvocationValue(hour, 23,
											crontab.hour));
							isHourModified = true;
						}
					} else {
						nextInvocationTime.add(Calendar.HOUR_OF_DAY, 1);
						isHourModified = true;
					}
				} else {
					// TODO bug
					if (hour >= CalendarUtil.get24Hour(currentTime)) {
						nextInvocationTime.set(
								Calendar.MINUTE,
								getFixedNextInvocationValue(minute, 59,
										crontab.minute));
					} else {
						nextInvocationTime.set(Calendar.MINUTE,
								getFixedInvocations(59, crontab.minute).get(0));
					}
					isMinuteModified = true;
				}
				loopCounter++;
			}
		} else {
			// check next invocation timestamp
			while ((isMonthModified && (!isDayModified || !isHourModified))
					|| nextInvocationTime.getTimeInMillis() < currentTime
							.getTimeInMillis()) {
				if (loopCounter >= 100) {
					throw new IllegalStateException(
							"Cannot get next invocation time of crontab scheduled task...");
				}
				if (isHourModified || isNotWildcard(crontab.hour)) {
					if (isHourModified || isFixedValue(crontab.hour)) {
						if (isDayModified || isNotWildcard(crontab.day)) {
							if (isDayModified || isFixedValue(crontab.day)) {
								if (isMonthModified
										|| isNotWildcard(crontab.month)) {
									if (isMonthModified
											|| isFixedValue(crontab.month)) {
										// cannot specify year at crontab
										nextInvocationTime
												.add(Calendar.YEAR, 1);
									} else {
										nextInvocationTime.set(
												Calendar.MONTH,
												getFixedNextInvocationValue(
														month, 12,
														crontab.month));
										isHourModified = true;
										isDayModified = true;
										isMonthModified = true;
									}
								} else {
									nextInvocationTime.add(Calendar.MONTH, 1);
									isHourModified = false;
									isDayModified = isFixedValue(crontab.day);
									isMonthModified = true;
									// renew hour
									hour = 0;
									if (isInvocationValueContainedInCrontabValue(
											0, 23, crontab.hour)) {
										nextInvocationTime.set(
												Calendar.HOUR_OF_DAY, 0);
										isHourModified = true;
									}
									// renew minute
									minute = 0;
									int newMinuteValue = 0;
									if (isFixedValue(crontab.minute)) {
										newMinuteValue = getFixedNextInvocationValue(
												minute, 59, crontab.minute);
									} else {
										newMinuteValue = getFixedNextInvocationValue(
												minute,
												crontab.minuteFixedInvocations);
									}
									nextInvocationTime.set(Calendar.MINUTE,
											newMinuteValue);
									// renew second
									nextInvocationTime.set(Calendar.SECOND, 0);
								}
							} else {
								nextInvocationTime.set(
										Calendar.DATE,
										getFixedNextInvocationValue(day, 31,
												crontab.day));
								isHourModified = true;
								isDayModified = true;
							}
						} else {
							nextInvocationTime.add(Calendar.DATE, 1);
							isHourModified = true;
							isDayModified = true;
						}
					} else {
						nextInvocationTime.set(
								Calendar.HOUR_OF_DAY,
								getFixedNextInvocationValue(hour, 23,
										crontab.hour));
						isHourModified = true;
					}
				} else {
					nextInvocationTime.add(Calendar.HOUR_OF_DAY, 1);
					isHourModified = true;
				}
				loopCounter++;
			}
		}
		return nextInvocationTime.getTimeInMillis();
	}

	/**
	 * replace wildcard values of minute/hour to the value array
	 * 
	 * @param crontab
	 *            crontab
	 * @return value array
	 */
	public static Crontab replaceWildcardsOfMinuteOrHourValue(Crontab crontab) {
		if (isWildcard(crontab.minute)) {
			StringBuilder sb = new StringBuilder();
			sb.append("0");
			for (int i = 1; i < 60; i++) {
				sb.append(",");
				sb.append(i);
			}
			crontab.minute = sb.toString();
			crontab.minuteFixedInvocations = getFixedInvocations(59,
					crontab.minute);
		}
		if (isWildcard(crontab.hour)) {
			StringBuilder sb = new StringBuilder();
			sb.append("0");
			for (int i = 1; i < 24; i++) {
				sb.append(",");
				sb.append(i);
			}
			crontab.hour = sb.toString();
			crontab.hourFixedInvocations = getFixedInvocations(23, crontab.hour);
		}
		return crontab;
	}

	/**
	 * is wildcard
	 * 
	 * @param value
	 *            string value
	 * @return result
	 */
	public static boolean isWildcard(String value) {
		return value.equals("*");
	}

	/**
	 * isNotWildcard
	 * 
	 * @param value
	 *            string value
	 * @return result
	 */
	public static boolean isNotWildcard(String value) {
		return !value.equals("*");
	}

	/**
	 * Day of week number value in crontab
	 * 
	 * @param cal
	 *            Calendar object
	 * @return day of week number value
	 */
	public static int getCrontabDayOfWeekNumber(Calendar cal) {
		return CalendarUtil.getDayOfWeekNumber(cal) - 1;
	}

	/**
	 * isFixedValue
	 * 
	 * @param value
	 *            string value
	 * @return result
	 */
	public static boolean isFixedValue(String value) {
		return value.matches("\\d+");
	}

	/**
	 * getFixedNextInvocationValue
	 * 
	 * @param currentValue
	 *            current value
	 * @param fixedInvocations
	 *            fixed invocations
	 * @return
	 */
	public static int getFixedNextInvocationValue(int currentValue,
			List<Integer> fixedInvocations) {
		if (fixedInvocations != null) {
			if (fixedInvocations.size() == 0) {
				throw new IllegalStateException("Invalid configuration...");
			}
			for (int fixed : fixedInvocations) {
				if (fixed > currentValue) {
					return fixed;
				}
			}
			return fixedInvocations.get(0);
		} else {
			return currentValue;
		}
	}

	/**
	 * getFixedNextInvocationValue
	 * 
	 * @param currentValue
	 *            current value
	 * @param maxValue
	 *            max value in the element
	 * @param crontabValue
	 *            crontab line
	 * @return result
	 */
	public static int getFixedNextInvocationValue(int currentValue,
			int maxValue, String crontabValue) {
		if (crontabValue.matches("^\\d+$")) {
			return Integer.valueOf(crontabValue);
		} else if (crontabValue.contains("/")
				|| crontabValue.matches("^[\\d,]+$")) {
			List<Integer> fixedInvocations = getFixedInvocations(maxValue,
					crontabValue);
			for (int fixed : fixedInvocations) {
				if (fixed > currentValue) {
					return fixed;
				}
			}
			return fixedInvocations.get(0);
		} else {
			throw new IllegalArgumentException(
					"Invalid crontab configuration value:" + crontabValue);
		}
	}

	/**
	 * Judge this working server is the one to invoke target job
	 * 
	 * @param crontab
	 *            crontab object
	 * @param serverName
	 *            working server name
	 * @return is server to invoke
	 */
	public static boolean isServerToInvoke(Crontab crontab, String serverName) {
		List<String> names = crontab.namedServers;
		if (names == null || names.size() == 0) {
			return true;
		}
		for (String name : names) {
			if (serverName.equals(name)) {
				return true;
			}
		}
		return false;
	}

	/**
	 * Judge crontab value contains invocation value or not
	 * 
	 * @param invocationValue
	 *            invocation value
	 * @param maxValue
	 *            max value
	 * @param crontabValue
	 *            crontab value
	 * @return is contained or not
	 */
	public static boolean isInvocationValueContainedInCrontabValue(
			int invocationValue, int maxValue, String crontabValue) {
		if (crontabValue.matches("^\\d+$")) {
			return invocationValue == Integer.valueOf(crontabValue);
		} else if (crontabValue.contains("/")
				|| crontabValue.matches("^[\\d,]+$")) {
			List<Integer> fixedInvocations = getFixedInvocations(maxValue,
					crontabValue);
			for (int fixed : fixedInvocations) {
				if (invocationValue == fixed) {
					return true;
				}
			}
			return false;
		}
		return false;
	}
}
